#region License, Terms and Author(s)
//
// ELMAH - Error Logging Modules and Handlers for ASP.NET
// Copyright (c) 2004-9 Atif Aziz. All rights reserved.
//
//  Author(s):
//
//      Atif Aziz, http://www.raboof.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

[assembly: Elmah.Scc("$Id: SQLiteErrorLog.cs 924 2011-12-23 22:41:47Z azizatif $")]

namespace Elmah
{

    #region Imports

    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Data.SQLite;
    using System.Globalization;
    using System.IO;

    using IDictionary = System.Collections.IDictionary;

    #endregion

    /// <summary>
    /// An <see cref="ErrorLog"/> implementation that uses SQLite as its backing store.
    /// </summary>
    public class SQLiteErrorLog : ErrorLog
    {
        /// <summary>
        /// Factory-type class to create parameters of the correct names and sizes.
        /// </summary>
        private static class MakeParam
        {
            public static SQLiteParameter ErrorId(Int64 value)
            {
                return new SQLiteParameter("@ErrorId", DbType.Int64) { Value = value };
            }

            public static SQLiteParameter PageIndex(int value)
            {
                return new SQLiteParameter("@PageIndex", DbType.Int16) { Value = value };
            }

            public static SQLiteParameter PageSize(int value)
            {
                return new SQLiteParameter("@PageSize", DbType.Int16) { Value = value };
            }

            public static SQLiteParameter Application(string value)
            {
                return new SQLiteParameter("@Application", DbType.String, 60) { Value = value };
            }

            public static SQLiteParameter Host(object value)
            {
                return new SQLiteParameter("@Host", DbType.String, 30) { Value = value };
            }

            public static SQLiteParameter Type(object value)
            {
                return new SQLiteParameter("@Type", DbType.String, 100) { Value = value };
            }

            public static SQLiteParameter Source(object value)
            {
                return new SQLiteParameter("@Source", DbType.String, 60) { Value = value };
            }

            public static SQLiteParameter Message(object value)
            {
                return new SQLiteParameter("@Message", DbType.String, 500) { Value = value };
            }

            public static SQLiteParameter User(object value)
            {
                return new SQLiteParameter("@User", DbType.String, 50) { Value = value };
            }

            public static SQLiteParameter StatusCode(int value)
            {
                return new SQLiteParameter("@StatusCode", DbType.Int64) { Value = value };
            }

            public static SQLiteParameter StatusCodeString(object value)
            {
                return new SQLiteParameter("@StatusCodeString", DbType.String, 100) { Value = value };
            }

            public static SQLiteParameter TimeUtc(DateTime value)
            {
                return new SQLiteParameter("@TimeUtc", DbType.DateTime) { Value = value };
            }

            public static SQLiteParameter TimeUtcFrom(object value)
            {
                return new SQLiteParameter("@TimeUtcFrom", DbType.DateTime) { Value = value };
            }

            public static SQLiteParameter TimeUtcTo(object value)
            {
                return new SQLiteParameter("@TimeUtcTo", DbType.DateTime) { Value = value };
            }

            public static SQLiteParameter AllXml(object value)
            {
                return new SQLiteParameter("@AllXml", DbType.String) { Value = value };
            }
        }

        private readonly string _connectionString;

        /// <summary>
        /// Initializes a new instance of the <see cref="SQLiteErrorLog"/> class
        /// using a dictionary of configured settings.
        /// </summary>

        public SQLiteErrorLog(IDictionary config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            string connectionString = ConnectionStringHelper.GetConnectionString(config, true);

            //
            // If there is no connection string to use then throw an 
            // exception to abort construction.
            //

            if (connectionString.Length == 0)
                throw new ApplicationException("Connection string is missing for the SQLite error log.");

            _connectionString = connectionString;

            InitializeDatabase();

            ApplicationName = config.Find("applicationName", string.Empty);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SQLiteErrorLog"/> class
        /// to use a specific connection string for connecting to the database.
        /// </summary>

        public SQLiteErrorLog(string connectionString)
        {
            if (connectionString == null)
                throw new ArgumentNullException("connectionString");

            if (connectionString.Length == 0)
                throw new ArgumentException(null, "connectionString");

            _connectionString = ConnectionStringHelper.GetResolvedConnectionString(connectionString);

            InitializeDatabase();
        }

        private static readonly object _lock = new object();
        private void InitializeDatabase()
        {
            string connectionString = ConnectionString;
            Debug.AssertStringNotEmpty(connectionString);

            string dbFilePath = ConnectionStringHelper.GetDataSourceFilePath(connectionString);

            if (File.Exists(dbFilePath))
                return;

            //
            // Make sure that we don't have multiple threads all trying to create the database
            //

            lock (_lock)
            {
                //
                // Just double check that no other thread has created the database while
                // we were waiting for the lock
                //

                if (File.Exists(dbFilePath))
                    return;

                SQLiteConnection.CreateFile(dbFilePath);

                const string sql = @"
                CREATE TABLE Error (
                    ErrorId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                    Application TEXT NOT NULL,
                    Host TEXT NOT NULL,
                    Type TEXT NOT NULL,
                    Source TEXT NOT NULL,
                    Message TEXT NOT NULL,
                    User TEXT NOT NULL,
                    StatusCode INTEGER NOT NULL,
                    TimeUtc TEXT NOT NULL,
                    AllXml TEXT NOT NULL
                )";

                using (SQLiteConnection connection = new SQLiteConnection(connectionString))
                using (SQLiteCommand cmd = new SQLiteCommand(sql, connection))
                {
                    connection.Open();
                    cmd.ExecuteNonQuery();
                }
            }
        }

        /// <summary>
        /// Gets the name of this error log implementation.
        /// </summary>

        public override string Name
        {
            get { return "SQLite Error Log"; }
        }

        /// <summary>
        /// Gets the connection string used by the log to connect to the database.
        /// </summary>

        public virtual string ConnectionString
        {
            get { return _connectionString; }
        }

        /// <summary>
        /// Logs an error to the database.
        /// </summary>
        /// <remarks>
        /// Use the stored procedure called by this implementation to set a
        /// policy on how long errors are kept in the log. The default
        /// implementation stores all errors for an indefinite time.
        /// </remarks>

        public override string Log(Error error)
        {
            if (error == null)
                throw new ArgumentNullException("error");

            string errorXml = ErrorXml.EncodeString(error);

            const string query = @"
                INSERT INTO Error (
                    Application, Host, 
                    Type, Source, Message, User, StatusCode, 
                    TimeUtc, AllXml)
                VALUES (
                    @Application, @Host, 
                    @Type, @Source, @Message, @User, @StatusCode, 
                    @TimeUtc, @AllXml);

                SELECT last_insert_rowid();";

            using (SQLiteConnection connection = new SQLiteConnection(ConnectionString))
            using (SQLiteCommand cmd = new SQLiteCommand(query, connection))
            {
                cmd.Parameters.Add(MakeParam.Application(ApplicationName));
                cmd.Parameters.Add(MakeParam.Host(error.HostName));
                cmd.Parameters.Add(MakeParam.Type(error.Type));
                cmd.Parameters.Add(MakeParam.Source(error.Source));
                cmd.Parameters.Add(MakeParam.Message(error.Message));
                cmd.Parameters.Add(MakeParam.User(error.User));
                cmd.Parameters.Add(MakeParam.StatusCode(error.StatusCode));
                cmd.Parameters.Add(MakeParam.TimeUtc(error.Time.ToUniversalTime()));
                cmd.Parameters.Add(MakeParam.AllXml(errorXml));
                connection.Open();

                return Convert.ToInt64(cmd.ExecuteScalar()).ToString(CultureInfo.InvariantCulture);
            }
        }

        public override bool SupportsSearching
        {
            get { return true; }
        }

        /// <summary>
        /// Returns a page of errors from the databse in descending order 
        /// of logged time.
        /// </summary>
        public override int GetErrors(SearchCriteria criteria, ICollection<ErrorLogEntry> errorEntryList)
        {
            ValidateCriteria(criteria);

            using (SQLiteConnection connection = new SQLiteConnection(ConnectionString))
            using (SQLiteCommand command = GetSqlCommand(criteria))
            {
                command.Connection = connection;
                connection.Open();

                using (SQLiteDataReader reader = command.ExecuteReader())
                {
                    if (errorEntryList != null)
                    {
                        while (reader.Read())
                        {
                            var id = Convert.ToString(reader["ErrorId"], CultureInfo.InvariantCulture);

                            var error = new Error
                            {
                                ApplicationName = reader["Application"].ToString(),
                                HostName = reader["Host"].ToString(),
                                Type = reader["Type"].ToString(),
                                Source = reader["Source"].ToString(),
                                Message = reader["Message"].ToString(),
                                User = reader["User"].ToString(),
                                StatusCode = Convert.ToInt32(reader["StatusCode"]),
                                Time = Convert.ToDateTime(reader["TimeUtc"]).ToLocalTime()
                            };

                            errorEntryList.Add(new ErrorLogEntry(this, id, error));
                        }
                    }

                    //
                    // Get the result of SELECT COUNT(*) FROM Page
                    //

                    reader.NextResult();
                    reader.Read();
                    return reader.GetInt32(0);
                }
            }
        }

        private SQLiteCommand GetSqlCommand(SearchCriteria criteria)
        {
            SQLiteCommand cmd;
            string sql = "";

            if (criteria.ContainsExtendedSearchCriteria)
            {
                sql = @"
                    SELECT
                        ErrorId,Application,Host,Type,Source,Message,User,StatusCode,TimeUtc
                    FROM
                        Error
                    WHERE
                        (Host like @Host or @Host is null)
                        and (Type like @Type or @Type is null)
                        and (Source like @Source or @Source is null)
                        and (Message like @Message or @Message is null)
                        and (User like @User or @User is null)
                        and (cast(StatusCode as varchar) like @StatusCodeString or @StatusCodeString is null)
                        and (TimeUtc >= @TimeUtcFrom OR @TimeUtcFrom IS NULL)
                        and (TimeUtc < @TimeUtcTo OR @TimeUtcTo IS NULL)
                    ORDER BY
                        ErrorId DESC
                    LIMIT 
                        @PageIndex * @PageSize,
                        @PageSize;

                    SELECT COUNT(*) FROM Error
                    WHERE
                        (Host like @Host or @Host is null)
                        and (Type like @Type or @Type is null)
                        and (Source like @Source or @Source is null)
                        and (Message like @Message or @Message is null)
                        and (User like @User or @User is null)
                        and (cast(StatusCode as varchar) like @StatusCodeString or @StatusCodeString is null)
                        and (TimeUtc >= @TimeUtcFrom OR @TimeUtcFrom IS NULL)
                        and (TimeUtc < @TimeUtcTo OR @TimeUtcTo IS NULL)
                    ";

                cmd = new SQLiteCommand(sql);
                cmd.Parameters.Add(MakeParam.PageIndex(criteria.PageIndex));
                cmd.Parameters.Add(MakeParam.PageSize(criteria.PageSize));
                cmd.Parameters.Add(MakeParam.Host(SqlUtils.GetSearchString(criteria.Host)));
                cmd.Parameters.Add(MakeParam.Type(SqlUtils.GetSearchString(criteria.TypeName)));
                cmd.Parameters.Add(MakeParam.Source(SqlUtils.GetSearchString(criteria.Source)));
                cmd.Parameters.Add(MakeParam.Message(SqlUtils.GetSearchString(criteria.Message)));
                cmd.Parameters.Add(MakeParam.User(SqlUtils.GetSearchString(criteria.User)));
                cmd.Parameters.Add(MakeParam.StatusCodeString(SqlUtils.GetSearchString(criteria.StatusCode)));
                cmd.Parameters.Add(MakeParam.TimeUtcFrom(SqlUtils.GetDateTimeParamValue(criteria.TimeUtcFrom)));
                cmd.Parameters.Add(MakeParam.TimeUtcTo(SqlUtils.GetDateTimeParamValue(criteria.TimeUtcTo)));
                cmd.Parameters.Add(MakeParam.AllXml(SqlUtils.GetSearchString(criteria.AllXml)));
            }
            else
            {
                sql = @"
                    SELECT
                        ErrorId,Application,Host,Type,Source,Message,User,StatusCode,TimeUtc
                    FROM
                        Error
                    ORDER BY
                        ErrorId DESC
                    LIMIT 
                        @PageIndex * @PageSize,
                        @PageSize;

                    SELECT COUNT(*) FROM Error";

                cmd = new SQLiteCommand(sql);
                cmd.Parameters.Add(MakeParam.PageIndex(criteria.PageIndex));
                cmd.Parameters.Add(MakeParam.PageSize(criteria.PageSize));
            }

            return cmd;
        }

        /// <summary>
        /// Returns the specified error from the database, or null 
        /// if it does not exist.
        /// </summary>
        public override ErrorLogEntry GetError(string id)
        {
            if (id == null)
                throw new ArgumentNullException("id");

            if (id.Length == 0)
                throw new ArgumentException(null, "id");

            long key;
            
            try
            {
                key = long.Parse(id, CultureInfo.InvariantCulture);
            }
            catch (FormatException e)
            {
                throw new ArgumentException(e.Message, "id", e);
            }

            const string sql = @"
                SELECT 
                    AllXml
                FROM 
                    Error
                WHERE
                    ErrorId = @ErrorId";

            using (SQLiteConnection connection = new SQLiteConnection(ConnectionString))
            using (SQLiteCommand command = new SQLiteCommand(sql, connection))
            {
                command.Parameters.Add(MakeParam.ErrorId(key));
                connection.Open();

                string errorXml = (string) command.ExecuteScalar();

                if (errorXml == null)
                    return null;

                Error error = ErrorXml.DecodeString(errorXml);
                return new ErrorLogEntry(this, id, error);
            }
        }
    }
}
